{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Ensemble Methods. Stacking and Blending. Useful tips"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/lets_stack.jpg\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "@Qovaxx"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Наверное каждый сталкивался с ситуацией, когда несколько дней многократных попыток повысить качество модели оказывались безрезультатными, будь то задание для курса или спортивное соревнование kaggle. Оказывается есть вариант, как пробить это плато и взять новый рубеж - ансамблирование моделей. Многие скажут, что тут все достаточно просто, - так и есть отвечу я, но это тоже искусство, которым необходимо овладеть.\n",
    "\n",
    "В текущем туториале мы приоткроем дверь в мир ансамблей и композиций моделей и попробуем слегка систематизировать информацию. Подробней остановимся на стэкинге и блендинге, их разных поюсах, минусах и реализациях. В конце вас ждет большая подборка советов для построения ансамблей. Ну что ж, поехали."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Общая идея ансамблирования: с помощью нескольких слабых классификаторов создать такое решающее правило, с помощью которого можно было бы повысить точность предсказания и сделать один сильный мета-классификатор."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Несколько основных причин создания ансамблей и композиций алгоритмов:\n",
    "* существует несколько классификаторов решающих одну задачу классификации **в разном контексте**. Например, идентификация человека по его лицу, голосу, почерку и т.д.;\n",
    "* иногда доступен более чем один набор данных, полученных из **разных** источников или созданных в **разные** периоды времени (Яндекс погода использует нечто подобное);\n",
    "* разные классификаторы тренированные на одном наборе данных могут не отличаться по качеству, но иметь сильные **локальные различия**. Так, каждый классификатор может иметь свою область признаков, где он сильнее остальных;\n",
    "* некоторые классификаторы (например нейронные сети) могут иметь разные результаты в зависимости от **инициализации**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Архитектуры"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Различные схемы комбинаций базовых классификаторов можно поделить на 3 большие группы:\n",
    "* **parallel** - базовые алгоритмы работают независимо (average, voting, stacking);\n",
    "* **cascading** - базовые алгоритмы выстраиваются в линейную последовательность (AdaBoost, GradientBoosting);\n",
    "* **hierarchical** - базовые алгоритмы выстраиваются в структуру напоминающую дерево решений (Hierarchical mixture of local experts)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Статичные комбинаторы по типу parallel (голосование, взвешенное, среднее), позволяют легко объединяться в команду с другими людьми, например в соревнованиях Kaggle. Таким образом вы независимо комбинируете ваши csv файлы с решениями, которые в свою очередь могут быть другими ансамблями.\n",
    "\n",
    "Не путайте hierarchical с многоуровневыми ансамблями."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Комбинаторы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Комбинатор - это модуль, стоящий над базовыми алгоритмами и преобразующий их пространство оценок в пространство ответов. Комбинаторы могут быть статичным и тренируемыми, адаптивными и не адаптивными. Под адаптивностью понимается, способность комбинатора оценивать вес для каждого базового алгоритма. Кроме того разные комбинаторы ожидают на вход разные входные данные: вероятности (confidence), ранк, лейблы классов (abstract). Вот некоторые варианты комбинаторов:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* **Voting, Weighted Voting** - parallel, abstract\n",
    "* **Sum, Min, Max, Mean, Median, Product** - parallel, confidence\n",
    "* **Generalized ensemble** - parallel, trainable, confidence\n",
    "* **Adaptive weighting** - parallel, trainable, adaptive, confidence\n",
    "* **Stacking, Blending, Feature-Weighted Linear Stacking** - parallel, trainable, confidence\n",
    "* **Borda count** - parallel, trainable, rank\n",
    "* **Logistic Regression over Ranks** - parallel, trainable, rank confidence\n",
    "* **Class set reductuin** - parallel cascading, trainable, rank confidence\n",
    "* **Dempsler-Shafor** - parallel, trainable, rank, confidence\n",
    "* **Fuzzy Integrals** - parallel, trainable, confidence\n",
    "* **Mixture of Local Experts** - parallel, trainable, adaptive, confidence\n",
    "* **Hierarchical Mixture of Local Experts** - hierarchical, parallel, trainable, adaptive, confidence\n",
    "* **Associate switch** - parallel, trainable, adaptive, abstract\n",
    "* **Gradient boosting, Adaboost, Arcing, BrownBoost, GentleBoost etc** - cascade, trainable, abstract\n",
    "* **Neural Tree** - hierarchical, trainable, confidence\n",
    "* **DECORATE (Diverse Ensemble Creation by Oppositional Relabeling of Artificial Training Examples)**\n",
    "* **Deep Stacking Network** - cascade, trainable\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Способы разнообразить ансамбли"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В основе созданий всех ансамблей лежит идея независимости и разнообразия. Пара данные-классификатор, лежащие в основе всех схем, должны быть максимально разнообразными. Несколько таких способов, с точки зрения данных:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* бэггинг\n",
    "* метод случайных подространств\n",
    "* бэггинг + метод случайных подпространств (как в random forest)\n",
    "* разное пространство признаков\n",
    "* разные тренировочные сессии - например применяется в случае нейронных сетей"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*бэггинг я отнес в эту категорию, так как по сути это метод создания различных подвыборок на основе бутстрапа, где для комбинации базовых алгоритмов используется голосование большинством или усреднение*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/stack_blendings.jpg\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Непосредственно стэкинг & блендинг"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Стекинг и блендинг - одни из самых известных методов ансамблирования, в котором за ансамбль отвечают не простые операции усреднения, а другой мета-алгоритм. Часто они превосходит по качеству базовые алгоритмы.\n",
    "\n",
    "Блендинг считается более простой версией стэкинга, хотя иногда люди используют эти термины взаимозаменяемо. Если в двух словах, то в блендинге тренировочные данные делятся на 2 части, по типу hold-out, а в стэкинге на несколько, по типу out-of-fold. Ниже поговорим об это более подробно.\n",
    "\n",
    "Давайте же взглянем на то, с помощью чего вытаскиваются топовые решения в соревнованиях kaggle."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*Для определенности будем решать задачу бинарной классификаци, где уровень ансамбля не превышает одного. Каждую схему ансамблирования запустим 10 раз, чтобы получить более точные результаты. Напомню, что здесь не стоит цель получить максимальное качество.*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "\n",
    "sns.set_style('darkgrid')\n",
    "sns.set(rc={'figure.figsize':(12, 10)})\n",
    "\n",
    "from numpy.random import normal\n",
    "from sklearn.datasets import make_moons\n",
    "from sklearn.metrics import roc_auc_score\n",
    "from sklearn.model_selection import StratifiedKFold, train_test_split\n",
    "from sklearn.utils import shuffle\n",
    "\n",
    "RANDOM_STATE = 17"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Возьмем искуственный пример с двухмерным пространством признаков. Линия классов сложная и нелинейная."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X, y = make_moons(n_samples=1000, random_state=RANDOM_STATE, noise=0.4)\n",
    "plt.scatter(X[:, 0], X[:, 1], c=pd.Series(y).map({0:'red', 1:'blue'}));"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=RANDOM_STATE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import GradientBoostingClassifier\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "from sklearn.svm import SVC"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Создадим много разных классификаторов, для которых проведем селекцию"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "classifiers = [KNeighborsClassifier(metric='manhattan', n_neighbors=52),\n",
    "               KNeighborsClassifier(metric='manhattan', n_neighbors=118),\n",
    "               KNeighborsClassifier(metric='chebyshev', n_neighbors=141),\n",
    "               KNeighborsClassifier(metric='chebyshev', n_neighbors=94),\n",
    "               KNeighborsClassifier(metric='euclidean', n_neighbors=27),\n",
    "               KNeighborsClassifier(metric='euclidean', n_neighbors=86),\n",
    "               LogisticRegression(C=32, penalty='l1', class_weight=None, fit_intercept=True, max_iter=150, tol=0.1),\n",
    "               LogisticRegression(C=0.005, penalty='l1', class_weight=None, fit_intercept=False, max_iter=100, tol=0.01),\n",
    "               LogisticRegression(C=0.0001, penalty='l2', class_weight=None, fit_intercept=True, max_iter=150, tol=1e-06),\n",
    "               LogisticRegression(C=96, penalty='l1', class_weight='balanced', fit_intercept=True, max_iter=50, tol=0.0001),\n",
    "               LogisticRegression(C=66, penalty='l2', class_weight='balanced', fit_intercept=False, max_iter=50, tol=0.1),\n",
    "               LogisticRegression(C=54, penalty='l2', class_weight='balanced', fit_intercept=False, max_iter=450, tol=0.001),\n",
    "               GradientBoostingClassifier(n_estimators=50, learning_rate=0.005, max_depth=12, max_features=0.8, min_samples_leaf=2, subsample=0.2),\n",
    "               GradientBoostingClassifier(n_estimators=50, learning_rate=0.01, max_depth=5, max_features=0.6, min_samples_leaf=10, subsample=0.8),\n",
    "               GradientBoostingClassifier(n_estimators=200, learning_rate=0.001, max_depth=90, max_features=0.5, min_samples_leaf=20, subsample=0.2),\n",
    "               GradientBoostingClassifier(n_estimators=150, learning_rate=50, max_depth=24, max_features=0.1, min_samples_leaf=28, subsample=0.3),\n",
    "               GradientBoostingClassifier(n_estimators=50, learning_rate=0.01, max_depth=20, max_features=0.6, min_samples_leaf=24, subsample=0.7),\n",
    "               GradientBoostingClassifier(n_estimators=500, learning_rate=1, max_depth=42, max_features=0.2, min_samples_leaf=26, subsample=0.9),\n",
    "               GradientBoostingClassifier(n_estimators=500, learning_rate=1, max_depth=22, max_features=0.8, min_samples_leaf=2, subsample=0.2),\n",
    "               SVC(probability=True, C=2, kernel='rbf'),\n",
    "               SVC(probability=True, C=0.005, kernel='rbf'),\n",
    "               SVC(probability=True, C=700, kernel='rbf'),\n",
    "               SVC(probability=True, C=0.5, degree=3, kernel='poly'),\n",
    "               SVC(probability=True, C=52, degree=2, kernel='poly'),\n",
    "               SVC(probability=True, C=72, degree=4, kernel='poly'),\n",
    "               SVC(probability=True, C=200, kernel='sigmoid'),\n",
    "               SVC(probability=True, C=2, kernel='sigmoid')]\n",
    "classifiers_names = ['knn1', 'knn2', 'knn3', 'knn4', 'knn5', 'knn6',\n",
    "                     'lr1', 'lr2', 'lr3', 'lr4', 'lr5', 'lr6',\n",
    "                     'gb1', 'gb2', 'gb3', 'gb4', 'gb5', 'gb6', 'gb7',\n",
    "                     'svm1', 'svm2', 'svm3', 'svm4', 'svm5', 'svm6', 'svm7', 'svm8']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "classifiers_predictions = pd.DataFrame()\n",
    "for name, classifier in zip(classifiers_names, classifiers):\n",
    "    classifier.fit(X_train, y_train)\n",
    "    train_predictions = classifier.predict_proba(X_train)[:, 1]\n",
    "    test_predictions = classifier.predict_proba(X_test)[:, 1]\n",
    "    \n",
    "    classifiers_predictions[name] = test_predictions\n",
    "    print('{0}: ({1} - {2})'.format(name,\n",
    "                                    round(roc_auc_score(y_train, train_predictions), 3),\n",
    "                                    round(roc_auc_score(y_test, test_predictions), 5)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Возьмем классификаторы, которые не переобучились и имеют auc больше 0.9"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "classifiers_predictions = classifiers_predictions[['knn1', 'knn2', 'knn3', 'knn4', 'knn5', 'knn6',\n",
    "                                                   'lr1', 'lr3', 'lr4', 'lr5', 'lr6',\n",
    "                                                   'gb1', 'gb2', 'gb3', 'gb5',\n",
    "                                                   'svm1', 'svm2', 'svm3', 'svm4']]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Построим корреляционную матрицу их ответов. Идея заключается в том, чтобы выбрать для ансамбля модели примерно одного качество и с наименьшей корреляцией"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.heatmap(classifiers_predictions.corr(), linewidths=.5);\n",
    "plt.yticks(rotation=0);\n",
    "plt.xticks(rotation=30);\n",
    "sns.set(font_scale=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Найдем менее коррелированные пары классификаторов (вы можете следовать какому-нибудь другому методу поиска таких моделей)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "min_correlation = np.argwhere(np.asarray(classifiers_predictions.corr()) < 0.9)\n",
    "selected_classifiers = list()\n",
    "for x, y in min_correlation:\n",
    "    selected_classifiers.append(classifiers_predictions.columns[x])\n",
    "    selected_classifiers.append(classifiers_predictions.columns[y])\n",
    "selected_classifiers = set(selected_classifiers)\n",
    "selected_classifiers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отберем их"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "classifiers = [KNeighborsClassifier(metric='manhattan', n_neighbors=52),\n",
    "               KNeighborsClassifier(metric='euclidean', n_neighbors=27),\n",
    "               KNeighborsClassifier(metric='euclidean', n_neighbors=86),\n",
    "               LogisticRegression(C=0.0001, penalty='l2', class_weight=None, fit_intercept=True, max_iter=150, tol=1e-06, random_state=RANDOM_STATE),\n",
    "               GradientBoostingClassifier(n_estimators=50, learning_rate=0.005, max_depth=12, max_features=0.8, min_samples_leaf=2, subsample=0.2, random_state=RANDOM_STATE), \n",
    "               GradientBoostingClassifier(n_estimators=50, learning_rate=0.01, max_depth=5, max_features=0.6, min_samples_leaf=10, subsample=0.8, random_state=RANDOM_STATE),\n",
    "               GradientBoostingClassifier(n_estimators=50, learning_rate=0.01, max_depth=20, max_features=0.6, min_samples_leaf=24, subsample=0.7, random_state=RANDOM_STATE),\n",
    "               SVC(probability=True, C=2, kernel='rbf', random_state=RANDOM_STATE),\n",
    "               SVC(probability=True, C=0.005, kernel='rbf', random_state=RANDOM_STATE),\n",
    "               SVC(probability=True, C=700, kernel='rbf', random_state=RANDOM_STATE)]\n",
    "\n",
    "classifiers_names = ['knn1','knn5', 'knn6',\n",
    "                     'lr3',\n",
    "                     'gb1', 'gb2',  'gb5',\n",
    "                     'svm1', 'svm2', 'svm3']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_decision_surface(clf, X_test, y_test, title):\n",
    "    xx, yy = np.meshgrid(np.linspace(-3, 3, 500), np.linspace(-3, 3, 500))\n",
    "\n",
    "    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])\n",
    "    Z = Z.reshape(xx.shape)\n",
    "\n",
    "    plt.contourf(xx, yy, Z, levels=np.linspace(Z.min(), Z.max(), 20), cmap=plt.cm.Spectral, alpha=0.5)\n",
    "\n",
    "    plt.scatter(X_test[:, 0], X_test[:, 1], c=pd.Series(y_test).map({0:'red', 1:'blue'}), s=20, alpha=1.0)\n",
    "    plt.xlim([-2, 3])\n",
    "    plt.ylim([-3, 3])\n",
    "    plt.axis('off')\n",
    "    plt.title(title)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Построим для них границу принятия решений"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "subplot_count = 1\n",
    "\n",
    "for name, classifier in zip(classifiers_names, classifiers):\n",
    "    plt.subplot(4, 3, subplot_count)\n",
    "    classifier.fit(X_train, y_train)\n",
    "    plot_decision_surface(classifier, X_test, y_test, name)\n",
    "    subplot_count += 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "xx, yy = np.meshgrid(np.linspace(-3, 3, 500), np.linspace(-3, 3, 500))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Простейшая схема блендинга"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тренировочную выборку делят на 2 части. Базовые алгоритмы обучают на 1 части и сразу же делают предсказания для второй части и для тестовых данных. Мета-признаки полученные на второй части идут на обучение мета-алгоритма, а мета-признаки тестовых данных уже используются для формирования финального предсказания.\n",
    "\n",
    "Преимущества блендинга:\n",
    "* проще стэкинга, меньшая вычислительная сложность;\n",
    "* защищен от утечки информации в отличие от стэкинга (причина в out-of-fold);\n",
    "* вам не нужно делиться seed от stratified folds с вашими напарниками.\n",
    "   \n",
    "Недостатки:\n",
    "* базовые алгоритмы и мета-алгоритм используют не всю обучающую выборку;\n",
    "* можно переобучиться на hold-out выборке;\n",
    "* out-of-fold более стабилен нежели использование просто hold-out выборки."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/simple_blending.png\">"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def simple_blending(basic_algorithms, meta_algorithm, X_train, X_test, y_train, part1_ratio=0.9, random_state=None):\n",
    "    X_train_part1, X_train_part2,\\\n",
    "    y_train_part1, y_train_part2 = train_test_split(X_train, y_train, test_size=1-part1_ratio, random_state=random_state)\n",
    "\n",
    "    meta_features_part2 = np.zeros((X_train_part2.shape[0], len(basic_algorithms)))\n",
    "    meta_features_test = np.zeros((X_test.shape[0], len(basic_algorithms)))\n",
    "\n",
    "    for index, basic_algorithm in enumerate(basic_algorithms):\n",
    "        basic_algorithm.fit(X_train_part1, y_train_part1)\n",
    "\n",
    "        part2_predictions = basic_algorithm.predict_proba(X_train_part2)[:, 1]\n",
    "        meta_features_part2[:, index] = part2_predictions\n",
    "\n",
    "        test_predictions = basic_algorithm.predict_proba(X_test)[:, 1]\n",
    "        meta_features_test[:, index] = test_predictions\n",
    "\n",
    "    meta_algorithm.fit(meta_features_part2, y_train_part2)\n",
    "\n",
    "    return meta_algorithm.predict_proba(meta_features_test)[:, 1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "experiments = list()\n",
    "for i in range(1, 10):\n",
    "    simple_blending_predictions = simple_blending(classifiers,\n",
    "                                              LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                              X_train, X_test, y_train,\n",
    "                                              part1_ratio=0.9,\n",
    "                                              random_state=i)\n",
    "    experiments.append(roc_auc_score(y_test, simple_blending_predictions))\n",
    "print('mean roc_auc: {0}\\nstd: {1}'.format(round(np.mean(experiments), 4), round(np.std(experiments), 5)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В простом варианте получили результаты сравнимые с лучшей моделью, но несомненно такой ансамбль гораздо стабильней."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "surface = simple_blending(classifiers,\n",
    "                    LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                    X_train, np.c_[xx.ravel(), yy.ravel()], y_train,\n",
    "                    part1_ratio=0.9,\n",
    "                    random_state=RANDOM_STATE).reshape(xx.shape)\n",
    "\n",
    "plt.contourf(xx, yy, surface, levels=np.linspace(surface.min(), surface.max(), 20), cmap=plt.cm.Spectral, alpha=0.7)\n",
    "plt.scatter(X_test[:, 0], X_test[:, 1], c=pd.Series(y_test).map({0:'red', 1:'blue'}), s=20, alpha=1.0)\n",
    "plt.xlim([-3, 3])\n",
    "plt.ylim([-3, 3])\n",
    "plt.axis('off')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Простейшая схема блендинга (признаки + мета-признаки)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Та же схема простейшего блендинга, но в этом случае мета-признаки добавлятся к обычным признаками, что на обучении, что на тесте. Такая реализация имеет право на существование, но все-таки лучше так не делать, так как возможно переобучение. Получается, что информация о целевой переменной \"зашита\" в общий набор признаков."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/simple_blending_features.png\">"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def simple_blending_features(basic_algorithms, meta_algorithm, X_train, X_test, y_train, part1_ratio=0.5, random_state=None):\n",
    "    X_train_part1, X_train_part2,\\\n",
    "    y_train_part1, y_train_part2 = train_test_split(X_train, y_train, test_size=1-part1_ratio, random_state=random_state)\n",
    "\n",
    "    meta_features_part2 = X_train_part2\n",
    "    meta_features_test = X_test\n",
    "\n",
    "    for index, basic_algorithm in enumerate(basic_algorithms):\n",
    "        basic_algorithm.fit(X_train_part1, y_train_part1)\n",
    "\n",
    "        part2_predictions = basic_algorithm.predict_proba(X_train_part2)[:, 1]\n",
    "        meta_features_part2 = np.hstack((meta_features_part2, part2_predictions.reshape(-1, 1)))\n",
    "\n",
    "        test_predictions = basic_algorithm.predict_proba(X_test)[:, 1]\n",
    "        meta_features_test = np.hstack((meta_features_test, test_predictions.reshape(-1, 1)))\n",
    "\n",
    "\n",
    "    meta_algorithm.fit(meta_features_part2, y_train_part2)\n",
    "\n",
    "    return meta_algorithm.predict_proba(meta_features_test)[:, 1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "experiments = list()\n",
    "for i in range(1, 10):\n",
    "    simple_blending_features_predictions = simple_blending_features(classifiers,\n",
    "                                                                    LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                                                    X_train, X_test, y_train,\n",
    "                                                                    part1_ratio=0.6,\n",
    "                                                                    random_state=i)\n",
    "    experiments.append(roc_auc_score(y_test, simple_blending_features_predictions))\n",
    "print('mean roc_auc: {0}\\nstd: {1}'.format(round(np.mean(experiments), 4), round(np.std(experiments), 5)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На удивление, такой прием дал большое преимущество, что в качестве, что в стабильности"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "surface = simple_blending_features(classifiers,\n",
    "                                   LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                   X_train, np.c_[xx.ravel(), yy.ravel()], y_train,\n",
    "                                   part1_ratio=0.6,\n",
    "                                   random_state=RANDOM_STATE).reshape(xx.shape)\n",
    "plt.contourf(xx, yy, surface, levels=np.linspace(surface.min(), surface.max(), 20), cmap=plt.cm.Spectral, alpha=0.7)\n",
    "plt.scatter(X_test[:, 0], X_test[:, 1], c=pd.Series(y_test).map({0:'red', 1:'blue'}), s=20, alpha=1.0)\n",
    "plt.xlim([-3, 3])\n",
    "plt.ylim([-3, 3])\n",
    "plt.axis('off')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Усреднение нескольких простейших блендингов с разным разбиением"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Логичным продолжением совершенствования схемы, является усреднение нескольких таких блендингов, используя разное разбиение тренировочной выборки. Таким образом базовые алгоритмы и мета-алгоритм теперь используют всю доступную обучающую выборку."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def average_blending(basic_algorithms, meta_algorithm, X_train, X_test, y_train, part1_ratio=0.9, n_iter=3, random_state=None):\n",
    "\n",
    "    simple_blending_realizations = list()\n",
    "    for iter in range(n_iter):\n",
    "        if random_state is None:\n",
    "            realization_random_state = None\n",
    "        else:\n",
    "            realization_random_state = iter + random_state\n",
    "\n",
    "        simple_blending_realizations.append(simple_blending(basic_algorithms,\n",
    "                                                            meta_algorithm,\n",
    "                                                            X_train, X_test, y_train,\n",
    "                                                            part1_ratio=part1_ratio,\n",
    "                                                            random_state=realization_random_state))\n",
    "    return np.mean(np.asarray(simple_blending_realizations), axis=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "experiments = list()\n",
    "for i in range(1, 10):\n",
    "    average_blending_predictions = average_blending(classifiers,\n",
    "                                                LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                                X_train, X_test, y_train,\n",
    "                                                part1_ratio=0.9,\n",
    "                                                n_iter=10,\n",
    "                                                random_state=i)\n",
    "    experiments.append(roc_auc_score(y_test, average_blending_predictions))\n",
    "print('mean roc_auc: {0}\\nstd: {1}'.format(round(np.mean(experiments), 4), round(np.std(experiments), 5)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Закономерно получили небольшое увеличение качества, сделав ансамбль более стабильным"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "surface = average_blending(classifiers,\n",
    "                           LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                           X_train, np.c_[xx.ravel(), yy.ravel()], y_train,\n",
    "                           part1_ratio=0.9,\n",
    "                           n_iter=10,\n",
    "                           random_state=RANDOM_STATE).reshape(xx.shape)\n",
    "plt.contourf(xx, yy, surface, levels=np.linspace(surface.min(), surface.max(), 20), cmap=plt.cm.Spectral, alpha=0.7)\n",
    "plt.scatter(X_test[:, 0], X_test[:, 1], c=pd.Series(y_test).map({0:'red', 1:'blue'}), s=20, alpha=1.0)\n",
    "plt.xlim([-3, 3])\n",
    "plt.ylim([-3, 3])\n",
    "plt.axis('off')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Усреднение нескольких простейших блендингов с разным разбиением (признаки + мета-признаки)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как еще один пример, реализация предыдущей схемы с объединением обычных признаков с мета-признаками. Далее, в следующих схемах, такой прием использоваться не будет, но если нужно то его очень легко добавить."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def average_blending_features(basic_algorithms, meta_algorithm, X_train, X_test, y_train, part1_ratio=0.5, n_iter=3, random_state=None):\n",
    "\n",
    "    simple_blending_realizations = list()\n",
    "    for iter in range(n_iter):\n",
    "        if random_state is None:\n",
    "            realization_random_state = None\n",
    "        else:\n",
    "            realization_random_state = iter + random_state\n",
    "\n",
    "        simple_blending_realizations.append(simple_blending_features(basic_algorithms,\n",
    "                                                                     meta_algorithm,\n",
    "                                                                     X_train, X_test, y_train,\n",
    "                                                                     part1_ratio=part1_ratio,\n",
    "                                                                     random_state=realization_random_state))\n",
    "    return np.mean(np.asarray(simple_blending_realizations), axis=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "experiments = list()\n",
    "for i in range(1, 10):\n",
    "    average_blending_features_predictions = average_blending_features(classifiers,\n",
    "                                                                      LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                                                      X_train, X_test, y_train,\n",
    "                                                                      part1_ratio=0.5,\n",
    "                                                                      n_iter=10,\n",
    "                                                                      random_state=i)\n",
    "    experiments.append(roc_auc_score(y_test, average_blending_features_predictions))\n",
    "print('mean roc_auc: {0}\\nstd: {1}'.format(round(np.mean(experiments), 4), round(np.std(experiments), 5)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Снова, на удивление всему что только можно, получили большой прирост в качестве, не потеряв в стабильности. Как и говорилось ранее, объединение признаков и мета-признаков иногда бывает лучше просто блендинга над мета-признаками."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "surface = average_blending_features(classifiers,\n",
    "                                    LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                    X_train, np.c_[xx.ravel(), yy.ravel()], y_train,\n",
    "                                    part1_ratio=0.5,\n",
    "                                    n_iter=10,\n",
    "                                    random_state=RANDOM_STATE).reshape(xx.shape)\n",
    "plt.contourf(xx, yy, surface, levels=np.linspace(surface.min(), surface.max(), 20), cmap=plt.cm.Spectral, alpha=0.7)\n",
    "plt.scatter(X_test[:, 0], X_test[:, 1], c=pd.Series(y_test).map({0:'red', 1:'blue'}), s=20, alpha=1.0)\n",
    "plt.xlim([-3, 3])\n",
    "plt.ylim([-3, 3])\n",
    "plt.axis('off')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Модификация блендинга"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Другая модификацией блендинга, которая призвана стать более эффективной в смысле достижимого качества. Общая идея такая: делим выборку на N частей, где каждую часть в свою очередь делим тоже на 2 части. Теперь эти N пар частей обучающей выборки используются для N реализация блендинга. Финальные предсказания всех этих блэндингов усредняются.\n",
    "\n",
    "Схема сложна в реализация и по факту оказывается не очень удачной. Можно модифицировать и этот вариант, опять таки усреднив несколько реализаций и осуществляя разбиения по принципу бутсрапа. Минус все тот же, не используется полный объем обучающей выборки."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/splits_blending.png\">"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def splits_blending(basic_algorithms, meta_algorithm, X_train, X_test, y_train, n_splits=3, part1_ratio=0.6, random_state=None):\n",
    "    X_train, y_train = shuffle(X_train, y_train, random_state=random_state)\n",
    "\n",
    "    splits_results = list()\n",
    "    for split_indexes in np.array_split(np.array(range(X_train.shape[0])), n_splits):\n",
    "        X_train_split = X_train[split_indexes]\n",
    "        y_train_split = y_train[split_indexes]\n",
    "\n",
    "        X_train_part1, X_train_part2, \\\n",
    "        y_train_part1, y_train_part2 = train_test_split(X_train_split, y_train_split, test_size=1 - part1_ratio,\n",
    "                                                        random_state=random_state)\n",
    "\n",
    "        meta_features_part2 = np.zeros((X_train_part2.shape[0], len(basic_algorithms)))\n",
    "        meta_features_test = np.zeros((X_test.shape[0], len(basic_algorithms)))\n",
    "        \n",
    "        for index, basic_algorithm in enumerate(basic_algorithms):\n",
    "            basic_algorithm.fit(X_train_part1, y_train_part1)\n",
    "\n",
    "            part2_predictions = basic_algorithm.predict_proba(X_train_part2)[:, 1]\n",
    "            meta_features_part2[:, index] = part2_predictions\n",
    "\n",
    "            test_predictions = basic_algorithm.predict_proba(X_test)[:, 1]\n",
    "            meta_features_test[:, index] = test_predictions\n",
    "\n",
    "\n",
    "        meta_algorithm.fit(meta_features_part2, y_train_part2)\n",
    "        splits_results.append(meta_algorithm.predict_proba(meta_features_test)[:, 1])\n",
    "\n",
    "\n",
    "    return np.mean(np.asarray(splits_results), axis=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "experiments = list()\n",
    "for i in range(1, 10):\n",
    "    splits_blending_predictions = splits_blending(classifiers,\n",
    "                                              LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                              X_train, X_test, y_train,\n",
    "                                              n_splits=3,\n",
    "                                              part1_ratio=0.8,\n",
    "                                              random_state=i)\n",
    "    experiments.append(roc_auc_score(y_test, splits_blending_predictions))\n",
    "print('mean roc_auc: {0}\\nstd: {1}'.format(round(np.mean(experiments), 4), round(np.std(experiments), 5)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ожидаемо, получили качество похуже, но все еще сравнимое с лучшей моделью, причем среднеквадратическое отклонение запусков снизилось в 3-4 раза."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "surface = splits_blending(classifiers,\n",
    "                          LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                          X_train, np.c_[xx.ravel(), yy.ravel()], y_train,\n",
    "                          n_splits=3,\n",
    "                          part1_ratio=0.8,\n",
    "                          random_state=RANDOM_STATE).reshape(xx.shape)\n",
    "plt.contourf(xx, yy, surface, levels=np.linspace(surface.min(), surface.max(), 20), cmap=plt.cm.Spectral, alpha=0.7)\n",
    "plt.scatter(X_test[:, 0], X_test[:, 1], c=pd.Series(y_test).map({0:'red', 1:'blue'}), s=20, alpha=1.0)\n",
    "plt.xlim([-3, 3])\n",
    "plt.ylim([-3, 3])\n",
    "plt.axis('off')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Конкатенация нескольких простейших блендингов с разным разбиением"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Следующий вариант модификации блендинга. Отличается тем, что мета-признаки второй части разных разбиений не сразу идут на обучение мета-алгоритму, а конкатенируются. Аналогичная ситуация и с мета-признаками тестовой выборки. В финальном векторе ответов от мета-алгоритма содержатся несколько ответов для каждого объекта, так как конкатенируются несколько тестовых выборок. Их усредняют.\n",
    "\n",
    "В итоге такая схема является более схожной в реализации и не гарантирует существенного прироста в качестве."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/concatenate_blending.png\">"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def concatenate_blending(basic_algorithms, meta_algorithm, X_train, X_test, y_train, part1_ratio=0.9, n_iter=3, random_state=None):\n",
    "\n",
    "    def blending_realization(basic_algorithms, X_train, y_train, X_test, part1_ratio, random_state):\n",
    "        X_train_part1, X_train_part2, \\\n",
    "        y_train_part1, y_train_part2 = train_test_split(X_train, y_train, test_size=1 - part1_ratio,\n",
    "                                                        random_state=random_state)\n",
    "\n",
    "        meta_features_part2 = np.zeros((X_train_part2.shape[0], len(basic_algorithms)))\n",
    "        meta_features_test = np.zeros((X_test.shape[0], len(basic_algorithms)))\n",
    "\n",
    "        for index, basic_algorithm in enumerate(basic_algorithms):\n",
    "            basic_algorithm.fit(X_train_part1, y_train_part1)\n",
    "\n",
    "            part2_predictions = basic_algorithm.predict_proba(X_train_part2)[:, 1]\n",
    "            meta_features_part2[:, index] = part2_predictions\n",
    "\n",
    "            test_predictions = basic_algorithm.predict_proba(X_test)[:, 1]\n",
    "            meta_features_test[:, index] = test_predictions\n",
    "\n",
    "        return (meta_features_part2, y_train_part2, meta_features_test)\n",
    "\n",
    "    realizations = list()\n",
    "    for iter in range(n_iter):\n",
    "        if random_state is None:\n",
    "            realization_random_state = None\n",
    "        else:\n",
    "            realization_random_state = iter + random_state\n",
    "\n",
    "        realizations.append(blending_realization(basic_algorithms, X_train, y_train, X_test, part1_ratio, random_state=realization_random_state))\n",
    "\n",
    "    X_meta = np.concatenate([x[0] for x in realizations])\n",
    "    y_meta = np.concatenate([x[1] for x in realizations])\n",
    "    X_meta_test = np.concatenate([x[2] for x in realizations])\n",
    "\n",
    "    meta_algorithm.fit(X_meta, y_meta)\n",
    "    predictions = meta_algorithm.predict_proba(X_meta_test)[:, 1]\n",
    "\n",
    "    return np.mean(predictions.reshape(-1, X_test.shape[0]), axis=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "experiments = list()\n",
    "for i in range(1, 10):\n",
    "    concatenate_blending_predictions = concatenate_blending(classifiers,\n",
    "                                                            LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                                            X_train, X_test, y_train,\n",
    "                                                            part1_ratio=0.9,\n",
    "                                                            n_iter=10,\n",
    "                                                            random_state=i)\n",
    "    experiments.append(roc_auc_score(y_test, concatenate_blending_predictions))\n",
    "print('mean roc_auc: {0}\\nstd: {1}'.format(round(np.mean(experiments), 4), round(np.std(experiments), 5)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "surface = concatenate_blending(classifiers,\n",
    "                               LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                               X_train, np.c_[xx.ravel(), yy.ravel()], y_train,\n",
    "                               part1_ratio=0.9,\n",
    "                               n_iter=10,\n",
    "                               random_state=RANDOM_STATE).reshape(xx.shape)\n",
    "plt.contourf(xx, yy, surface, levels=np.linspace(surface.min(), surface.max(), 20), cmap=plt.cm.Spectral, alpha=0.7)\n",
    "plt.scatter(X_test[:, 0], X_test[:, 1], c=pd.Series(y_test).map({0:'red', 1:'blue'}), s=20, alpha=1.0)\n",
    "plt.xlim([-3, 3])\n",
    "plt.ylim([-3, 3])\n",
    "plt.axis('off')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Классический стэкинг с усредненим предсказаний"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Наконец мы добрались до самого стэкинга, призванного решить проблему использования всей обучающей выборки."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выборку делят на несколько фолдов, все базовые алгоритмы обучают на всех фолдах кроме одного, а для последнего оставшегося делают предсказания. Так получаются мета-признаки для текущей части. Это процесс повторяется несколько раз, пока каждый фолд не выступит в роли \"предсказательного\". Сконкатенируя их вместе получим данные для обучения мета-алгоритма.\n",
    "\n",
    "Мета-признаки для теста получают также во время тасовки фолдов. Базовые алгоритмы, обученные на всех фолдах кроме одного, делают предсказания для тестовой выборки, причем сколько фолдов столько и вариантов предсказаний. Все полученные мета-признаки для теста усредняют и подают на вход мета-алгоритму. Так же как и в блендинге, в стэкинге желательно сделать несколько разных разбиений на фолды, которые потом усредняются.\n",
    "\n",
    "В случае когда K = |X| получается случай по типу leave-one-out."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Преимущества:\n",
    "* используется вся обучающая выборка\n",
    "* стабильная схема в плане достижения качества\n",
    "\n",
    "Минусом является то, что мета-признаки для обучения мета-алгоритма и мета-признаки тестовой части разные, это является следствием следующих недостатков:\n",
    "* тренировочные мета-признаки состоят из разных кусочков, полученных от разных базовых алгоритмов с разными гиперпараметрами\n",
    "* усреднение меняет тестовые значения мета-признаков, например {0, 1} значения мета-признаков на трейне могут стать числами с плавающей точкой на тесте\n",
    "\n",
    "С первым недостатком борются с помощью наложения нормального шума на мета-признаки, что является своего рода регуляризацией"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/average_kfold_stacking.png\">"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def average_kfold_stacking(basic_algorithms, meta_algorithm, X_train, X_test, y_train, n_folds=3, n_iter=3, noise_scale=0, random_state=None):\n",
    "\n",
    "    realizations = list()\n",
    "\n",
    "    for iter in range(n_iter):\n",
    "        if random_state is None:\n",
    "            realization_random_state = None\n",
    "        else:\n",
    "            realization_random_state = iter + random_state\n",
    "\n",
    "        folds_results = list()\n",
    "        skf = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=realization_random_state)\n",
    "        for train_index, test_index in skf.split(X_train, y_train):\n",
    "            X_train_folds, X_test_fold = X_train[train_index], X_train[test_index]\n",
    "            y_train_folds, y_test_fold = y_train[train_index], y_train[test_index]\n",
    "\n",
    "            fold_meta_features = np.zeros((X_test_fold.shape[0], len(basic_algorithms)))\n",
    "            meta_features_test = np.zeros((X_test.shape[0], len(basic_algorithms)))\n",
    "\n",
    "            for index, basic_algorithm in enumerate(basic_algorithms):\n",
    "                basic_algorithm.fit(X_train_folds, y_train_folds)\n",
    "\n",
    "                test_fold_predictions = basic_algorithm.predict_proba(X_test_fold)[:, 1]\n",
    "                fold_meta_features[:, index] = normal(test_fold_predictions, scale=noise_scale)\n",
    "\n",
    "                test_predictions = basic_algorithm.predict_proba(X_test)[:, 1]\n",
    "                meta_features_test[:, index] = normal(test_predictions, scale=noise_scale)\n",
    "\n",
    "            folds_results.append((fold_meta_features, y_test_fold, meta_features_test))\n",
    "\n",
    "        meta_features = np.concatenate([x[0] for x in folds_results])\n",
    "        meta_y = np.concatenate([x[1] for x in folds_results])\n",
    "\n",
    "        meta_test = np.mean(np.asarray([x[2] for x in folds_results]), axis=0)\n",
    "\n",
    "        meta_algorithm.fit(meta_features, meta_y)\n",
    "        realizations.append(meta_algorithm.predict_proba(meta_test)[:, 1])\n",
    "\n",
    "    return np.mean(np.asarray(realizations), axis=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "experiments = list()\n",
    "for i in range(1, 10):\n",
    "    average_kfold_stacking_predictions = average_kfold_stacking(classifiers,\n",
    "                                                    LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                                    X_train, X_test, y_train,\n",
    "                                                    n_folds=5,\n",
    "                                                    n_iter=5,\n",
    "                                                    noise_scale=0,\n",
    "                                                    random_state=i)\n",
    "    experiments.append(roc_auc_score(y_test, average_kfold_stacking_predictions))\n",
    "print('mean roc_auc: {0}\\nstd: {1}'.format(round(np.mean(experiments), 4), round(np.std(experiments), 5)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В данном случае стэкинг работет получше большинста блендингов, что в плане качестве, что в плане стабильности. Давайте попробуем добавить шум для мета-признаков:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "experiments = list()\n",
    "for i in range(1, 10):\n",
    "    average_kfold_stacking_predictions = average_kfold_stacking(classifiers,\n",
    "                                                    LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                                    X_train, X_test, y_train,\n",
    "                                                    n_folds=5,\n",
    "                                                    n_iter=5,\n",
    "                                                    noise_scale=0.02,\n",
    "                                                    random_state=i)\n",
    "    experiments.append(roc_auc_score(y_test, average_kfold_stacking_predictions))\n",
    "print('mean roc_auc: {0}\\nstd: {1}'.format(round(np.mean(experiments), 4), round(np.std(experiments), 5)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Не трудно догадаться, что это было лишним, но возможно стоит понастраивать разные параметры самого стэкинга."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "surface = average_kfold_stacking(classifiers,\n",
    "                                 LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                 X_train, np.c_[xx.ravel(), yy.ravel()], y_train,\n",
    "                                 n_folds=5,\n",
    "                                 n_iter=5,\n",
    "                                 noise_scale=0,\n",
    "                                 random_state=RANDOM_STATE).reshape(xx.shape)\n",
    "plt.contourf(xx, yy, surface, levels=np.linspace(surface.min(), surface.max(), 20), cmap=plt.cm.Spectral, alpha=0.7)\n",
    "plt.scatter(X_test[:, 0], X_test[:, 1], c=pd.Series(y_test).map({0:'red', 1:'blue'}), s=20, alpha=1.0)\n",
    "plt.xlim([-3, 3])\n",
    "plt.ylim([-3, 3])\n",
    "plt.axis('off')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Классический стэкинг"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Собственно отличие от предыдущей реализации в том, что мета-признаки для тестовой части делают не базовые алгоритмы, обученные на фолдах, а совершенно новые, обученные на всей обучающей выборке, включающую все фолды. Мета-признаки конкатенируются по колонкам и отправляются мета-алгоритму на вход.\n",
    "\n",
    "И точно также как и в предыдущей реализация мета-признаки теста и трейна разные, только в этот раз признаки на контроле являются ответом совершенно других, снова обученных базовых алгоритмов."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"../../img/classical_stacking.png\">"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def classical_stacking(basic_algorithms, meta_algorithm, X_train, X_test, y_train, n_folds=3, n_iter=3, noise_scale=0, random_state=None):\n",
    "\n",
    "    realizations = list()\n",
    "\n",
    "    for iter in range(n_iter):\n",
    "        if random_state is None:\n",
    "            realization_random_state = None\n",
    "        else:\n",
    "            realization_random_state = iter + random_state\n",
    "\n",
    "        folds_results = list()\n",
    "        skf = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=realization_random_state)\n",
    "        for train_index, test_index in skf.split(X_train, y_train):\n",
    "            X_train_folds, X_test_fold = X_train[train_index], X_train[test_index]\n",
    "            y_train_folds, y_test_fold = y_train[train_index], y_train[test_index]\n",
    "\n",
    "            fold_meta_features = np.zeros((X_test_fold.shape[0], len(basic_algorithms)))\n",
    "\n",
    "            for index, basic_algorithm in enumerate(basic_algorithms):\n",
    "                basic_algorithm.fit(X_train_folds, y_train_folds)\n",
    "                test_fold_predictions = basic_algorithm.predict_proba(X_test_fold)[:, 1]\n",
    "                fold_meta_features[:, index] = normal(test_fold_predictions, scale=noise_scale)\n",
    "\n",
    "            folds_results.append((fold_meta_features, y_test_fold))\n",
    "\n",
    "        meta_features = np.concatenate([x[0] for x in folds_results])\n",
    "        meta_y = np.concatenate([x[1] for x in folds_results])\n",
    "\n",
    "\n",
    "        meta_features_test = np.zeros((X_test.shape[0], len(basic_algorithms)))\n",
    "        for index, basic_algorithm in enumerate(basic_algorithms):\n",
    "            basic_algorithm.fit(X_train, y_train)\n",
    "            test_predictions = basic_algorithm.predict_proba(X_test)[:, 1]\n",
    "            meta_features_test[:, index] = normal(test_predictions, scale=noise_scale)\n",
    "\n",
    "        meta_algorithm.fit(meta_features, meta_y)\n",
    "\n",
    "        realizations.append(meta_algorithm.predict_proba(meta_features_test)[:, 1])\n",
    "\n",
    "    return np.mean(np.asarray(realizations), axis=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "experiments = list()\n",
    "for i in range(1, 10):\n",
    "    classical_stacking_predictions = classical_stacking(classifiers,\n",
    "                                                    LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                                    X_train, X_test, y_train,\n",
    "                                                    n_folds=5,\n",
    "                                                    n_iter=5,\n",
    "                                                    noise_scale=0,\n",
    "                                                    random_state=i)\n",
    "    experiments.append(roc_auc_score(y_test, classical_stacking_predictions))\n",
    "print('mean roc_auc: {0}\\nstd: {1}'.format(round(np.mean(experiments), 4), round(np.std(experiments), 5)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На данной выборке, такая реализация стэкинга рабоет похуже, бывает."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "experiments = list()\n",
    "for i in range(1, 10):\n",
    "    classical_stacking_predictions = classical_stacking(classifiers,\n",
    "                                                    LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                                                    X_train, X_test, y_train,\n",
    "                                                    n_folds=5,\n",
    "                                                    n_iter=5,\n",
    "                                                    noise_scale=0.02,\n",
    "                                                    random_state=i)\n",
    "    experiments.append(roc_auc_score(y_test, classical_stacking_predictions))\n",
    "print('mean roc_auc: {0}\\nstd: {1}'.format(round(np.mean(experiments), 4), round(np.std(experiments), 5)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "И точно также шум не помог."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "surface = classical_stacking(classifiers,\n",
    "                             LogisticRegression(C=5, random_state=RANDOM_STATE),\n",
    "                             X_train, np.c_[xx.ravel(), yy.ravel()], y_train,\n",
    "                             n_folds=5,\n",
    "                             n_iter=5,\n",
    "                             noise_scale=0,\n",
    "                             random_state=RANDOM_STATE).reshape(xx.shape)\n",
    "plt.contourf(xx, yy, surface, levels=np.linspace(surface.min(), surface.max(), 20), cmap=plt.cm.Spectral, alpha=0.7)\n",
    "plt.scatter(X_test[:, 0], X_test[:, 1], c=pd.Series(y_test).map({0:'red', 1:'blue'}), s=20, alpha=1.0)\n",
    "plt.xlim([-3, 3])\n",
    "plt.ylim([-3, 3])\n",
    "plt.axis('off')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Существуют и другие реализации стэкинга, в том числе и улучшение предыдущей схемы, но в целом основная идея такая же как и в предыдущих схемах. Теперь зная как это работает, вы можете написать и адаптировать стэкинг и блендинг под любую задачу)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Заметки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* Не всегда блендинг и стэкинг **существенно превышают** качество лучшего из базовых алгоритмов, но почти всегда такой ансамбль оказывается **намного стабильней** одиночного решения, что дает плюс в копилку на соревнованиях;\n",
    "* Если из ансамбля убрать лучший базовый алгоритм, то финальное качество падает не сильно;\n",
    "* Традиционно в бустинге и бэггинге используются алгоритмы одной природы, в стэкинге и блендинге разной;\n",
    "* При умелом подходе и умеренном использованни стэкинг применяется в реальных бизнес задачах, например, если нужно очень стабильное решение в угоду интерпретированности;\n",
    "* В задаче бинарной классификации, с функцией ошибки log_loss, тяжело придумать ансамбль, который их улучшит."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"./../img/kaggle.png\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Практические советы для стекинга и не только"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* Чтобы выжать максимум качества, для конкретных решений пишите конкретные реализации ансамблей, библиотеки же можно использовать для первых заходов;\n",
    "* Самое важное, используйте стекинг только тогда, когда вы уверены, что выжали максимум информации из данных. Один \"хороший признак\" под конкретную модель стоит нескольких ансамблей\n",
    "* Пристыковывать мета-признаки к обычным признакам в этих семах лучше не стоит, но иногда все-таки может быть улучшение качества;\n",
    "* Добавляйте шум (возможно не только нормальный) к мета-признакам для \"регуляризации\", если это дает прирост в качестве;\n",
    "* Используйте стэкинг преимущественно на больших выборках, измеряющихся десятками тысяч. На маленьких выборках нужно аккуратно подбирать базовые и мета- алгоритмы;\n",
    "* В стэкинге и блендинге используйте алгоритмы разной природы (ближайшие соседи, линейные, деревянные, нейронные сети и др);\n",
    "* Иногда может оказаться полезной деформация признакового пространства, то есть вместо мета-признаков (ответы моделей) использовать мономы над ними, например все попарные произведения;\n",
    "* Не забывайте настраивать параметры самого стэкинга, так чтобы было максимальное качество за приемлемое время работы;\n",
    "* Мета-алгоритм должен оптимизировать ваш функционал, от базовых алгоритмов это не требуется. Речь идет об 1 уровневом ансамбле;\n",
    "* Можно обобщить несколько стекингов и блендингов в еще один стекинг (многоуровневый), но делайте так только с спротивных задачах, например при работе в команде;\n",
    "* Стэкинг - это machine learning над сильно скоррелированными между собой мета-признаками и целевой переменной, поэтому предъявите особые требования к регуляризации;\n",
    "* Используйте базовые алгоритмы с плохокоррелированными ошибками, такое сочетание позволяет достичь значительно большего качества. Например можно построить корреляционную матрицу Пирсона над ответами моделей и выбрать наименее скоррелированные;\n",
    "* В больших ансамблях лучше использовать слабо регуляризованные классификаторы, которые фактически переобучены на обучающей выборке, это позволяет сократить дисперсию;\n",
    "* Метод случайных подпространств работают лучше всего на выборках с большим числом признаков и не совсем малым числом примеров;\n",
    "* Практический опыт: нет смысла использовать разные классификаторы на одном и том же множестве признаков, зато целесообразно использовать один классификатор на разных подмножествах признаков;\n",
    "* В случае значительных шумов бэггинг намного лучше бустинга;\n",
    "* Бэггинг подходит для слабых нестабильных классификаторов с неубывающей кривой обучения (learning curve). Бустинг хорош в тех же условиях, но для больших размеров обучающей выборки. Метод случайного подпространства показывает неплохие характеристики в случае нестабильных слабых классификаторов с убывающей кривой обучения и малых размеров обучающей выборки;\n",
    "* Ансамбль svm не приводит к улучшению точности по сравнению с одной правильно настроенной машиной, однако ансамбли более стабильны;\n",
    "* DECORATE побеждает отдельно взятые классификаторы, бэггинг и случайные леса, а также превосходит AdaBoost на малых выборках и сравним на больших;\n",
    "* Бэггинг особенно эффективен на малых выборках;\n",
    "* Можно добавать кластеризацию данных как мета-признак, например kmeans или 2-3 признака от t-SNE;\n",
    "* Геометрическое усреднение чаще превосходит обычное;\n",
    "* Для объединения в ансамбль плохо откалибровнных алгоритмов можно использовать rank average. Это особенно хорошо работает на ранках и threshold-based метриках типа AUC;\n",
    "* Feature-WeightedLinear Stacking реализован в Vowpal Wabbit;\n",
    "* Хорошей практикой является использование классификаторов для ансамблей с задачей регрессии. Так первоначальный вектор y делят на зоны, которые и становятся своебразными классами. Такой классификатор должен помочь в ансамбле регрессоров;\n",
    "* Одни из вариантов построения ансамблей: жадный, где ансамбли добавляются в композицию, только в случае общего улучшения качестве и брутфорс, при котором строится множество случайных ансамблей и выбирается лучший(ие)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Выводы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Резюмируя, вы ознакомились с ансамблированием и заглянули под капот таким схемам как блендинг и стэкинг. Теперь при возможности с легкостью написать их самим и адаптировать под конкретную задачу. Кроме того, можно реализовать и другие методы из списка в самом начале, благо статьи на эти темы есть."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Пару слов о датасете, использованного в этом тьюториале. Он оказался не очень показательным, так как конкретно эту задачу классификации ближайшие соседи решают максимально близко к возможному, поэтому ансамбли не дают существенного прироста. Но никто и не говорил, что смесь моделей всегда будет превосходить отдельные модели. Тем не менее на kaggle, если вы вверху списка публичного рейтинга, любые улучшения дадут вам несколько позиций вверх. Кто знает, может это будет золото?)\n",
    "\n",
    "Как бонус, используйте готовые библиотеки [brew](https://github.com/viisar/brew) и [heamy](https://github.com/rushter/heamy)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Список литературы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[1] [Cтекинг (Stacking) и блендинг (Blending)](https://alexanderdyakonov.wordpress.com/2017/03/10/c%D1%82%D0%B5%D0%BA%D0%B8%D0%BD%D0%B3-stacking-%D0%B8-%D0%B1%D0%BB%D0%B5%D0%BD%D0%B4%D0%B8%D0%BD%D0%B3-blending/)\n",
    "\n",
    "[2] [История развития ансамблевых методов классификации в машинном обучении](https://www.researchgate.net/publication/278019662_Istoria_razvitia_ansamblevyh_metodov_klassifikacii_v_masinnom_obucenii?enrichId=rgreq-4227f37e04a649e6c2644493d49b3b8a-XXX&enrichSource=Y292ZXJQYWdlOzI3ODAxOTY2MjtBUzoyMzg4OTM5NDk3Nzk5NjhAMTQzMzk2ODE2NDM0Mg%3D%3D&el=1_x_3&_esc=publicationCoverPdf)\n",
    "\n",
    "[3] [Методы ансамблирования обучающихся алгоритмов](http://www.machinelearning.ru/wiki/images/5/56/Guschin2015Stacking.pdf)\n",
    "\n",
    "[4] [Градиентный бустинг лекция 9](https://github.com/esokolov/ml-course-hse/blob/master/2016-fall/lecture-notes/lecture09-ensembles.pdf)\n",
    "\n",
    "[5] [Градиентный бустинг лекция 10](https://github.com/esokolov/ml-course-hse/blob/master/2016-fall/lecture-notes/lecture10-ensembles.pdf)\n",
    "\n",
    "[6] [A Kaggler's Guide to Model Stacking in Practice](http://blog.kaggle.com/2016/12/27/a-kagglers-guide-to-model-stacking-in-practice/)\n",
    "\n",
    "[7] [KAGGLE ENSEMBLING GUIDE](https://mlwave.com/kaggle-ensembling-guide/)\n",
    "\n",
    "[8] [Statistical Pattern Recognition](http://www4.comp.polyu.edu.hk/~csajaykr/myhome/teaching/biometrics/spr_pami.pdf)\n",
    "\n",
    "[9] [Stacked regressions](https://rd.springer.com/article/10.1007%2FBF00117832)\n",
    "\n",
    "[10] [Bagging Predictors](https://rd.springer.com/article/10.1023%2FA%3A1018054314350)\n",
    "\n",
    "[11] [Feature-Weighted Linear Stacking](https://arxiv.org/pdf/0911.0460.pdf?lipi=urn%253Ali%253Apage%253Ad_flagship3_pulse_read%253BPZ4T3JLHTu%252BOWNI0d5kFbg%253D%253D)\n",
    "\n",
    "[12] [Combining Predictions for Accurate Recommender Systems](http://elf-project.sourceforge.net/CombiningPredictionsForAccurateRecommenderSystems.pdf?lipi=urn%3Ali%3Apage%3Ad_flagship3_pulse_read%3BPZ4T3JLHTu%2BOWNI0d5kFbg%3D%3D)\n",
    "\n",
    "[13] [The BigChaos Solution to the Netflix Grand Prize](https://www.netflixprize.com/assets/GrandPrize2009_BPC_BigChaos.pdf)\n",
    "\n",
    "[14] [Computer Vision for Head Pose Estimation: Review of a Competition](http://vision.cs.tut.fi/data/publications/scia2015_hpe.pdf)\n",
    "\n",
    "[15] [Ensemble selection from libraries models](http://www.cs.cornell.edu/~caruana/ctp/ct.papers/caruana.icml04.icdm06long.pdf)\n",
    "\n",
    "[16] [The sum is greater than the parts](http://www.columbia.edu/~rsb2162/PBGH-SIGKDDExp.pdf)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
